#include <stdio.h>// printf() 函数用于格式化输出。
#include <stdlib.h>// exit() 函数用于退出程序。
#include <string.h>// strtok() 函数用于分割字符串。
#include <assert.h>// assert() 函数用于断言。
#include <unistd.h>// fork() 函数用于创建子进程。
#include <stdlib.h>// getenv() 函数用于获取环境变量。
#include <sys/types.h>// waitpid() 函数用于等待子进程结束。
#include <sys/wait.h>// WEXITSTATUS() 函数用于获取子进程的退出状态码。
#include <ctype.h>// isspace() 函数用于判断空格。
#include <fcntl.h>// open() 函数用于打开文件。


#define LEFT "["// 'LEFT' 定义了命令行提示符的左括号。
#define RIGHT "]"// 'RIGHT' 定义了命令行提示符的右括号。
#define LABLE "#"// 'LABLE' 定义了命令行提示符的标签。
#define DELIM " \t"// 'DELIM' 定义了命令行参数分隔符。
#define LINE_SIZE 1024// 'LINE_SIZE' 定义了命令行字符数组的最大长度。
#define ARGC_SIZE 32// 'ARGC_SIZE' 定义了命令行参数数组的最大长度。
#define EXIT_CODE 44// 'EXIT_CODE' 定义了命令执行失败时的退出码。

#define NONE -1// 'NONE' 定义了重定向类型未定义时的常量。
#define IN_RDIR     0// 'IN_RDIR' 定义了输入重定向类型。
#define OUT_RDIR    1// 'OUT_RDIR' 定义了输出重定向类型。
#define APPEND_RDIR 2// 'APPEND_RDIR' 定义了追加重定向类型。

int lastcode = 0;// 'lastcode' 存储上一个命令的退出状态码。
int quit = 0;// 'quit' 标记是否退出程序。
extern char **environ;// 定义环境变量表。
char commandline[LINE_SIZE];// 'commandline' 存储用户输入的命令行字符串。
char *argv[ARGC_SIZE];         // 'argv' 存储解析后的命令参数数组。
char pwd[LINE_SIZE];           // 'pwd' 存储当前工作目录的路径。
char *rdirfilename = NULL;     // 'rdirfilename' 存储重定向的文件名。
int rdir = NONE;               // 'rdir' 标记当前的重定向类型。

// 自定义环境变量表
char myenv[LINE_SIZE];         // 'myenv' 存储自定义的环境变量字符串。
// 自定义本地变量表


// 辅助函数的定义: 'getusername()' 和 'gethostname1()' 这两个函数通过 'getenv()' 函数获取当前用户和主机名，用于命令行提示符的显示。
const char *getusername()// 定义了获取用户名的函数。
{
    return getenv("USER");// 获取当前用户的用户名。
}

const char *gethostname1()// 定义了获取主机名的函数。
{
    return getenv("HOSTNAME");// 获取当前主机的主机名。
}

// 'getpwd()' 函数获取当前工作目录的路径，并将其存储在pwd数组中。
void getpwd()
{
    getcwd(pwd, sizeof(pwd));// 获取当前工作目录的路径。
}

// 'check_redir()' 函数用于检查命令字符串中是否包含重定向符号 <, >, >>，并根据符号类型设置 rdir 变量和 rdirfilename 变量，用于后续重定向操作。
void check_redir(char *cmd)
{

    // ls -al -n
    // ls -al -n >/</>> filename.txt
    char *pos = cmd;// 指针指向命令字符串的第一个字符。
    while(*pos)// 遍历整个命令字符串
    {
        if(*pos == '>')// 找到输出重定向符号 '>'
        {
            if(*(pos+1) == '>'){// 找到追加重定向符号 '>>'
                *pos++ = '\0';// 将 '>' 符号替换为 '\0'，将 '>>' 符号替换为空格。
                *pos++ = '\0';// 将 '>' 符号后面的空格替换为空格。
                while(isspace(*pos)) pos++;// 跳过空格。
                rdirfilename = pos;// 记录重定向的文件名。
                rdir=APPEND_RDIR;// 设置重定向类型为追加。
                break;// 跳出循环。
            }
            else{// 找到输出重定向符号 '>'
                *pos = '\0';// 将 '>' 符号替换为 '\0'。
                pos++;// 指向重定向符号后面的字符。
                while(isspace(*pos)) pos++;// 跳过空格。
                rdirfilename = pos;// 记录重定向的文件名。
                rdir=OUT_RDIR;// 设置重定向类型为输出。
                break;// 跳出循环。
            }
        }
        else if(*pos == '<')// 找到输入重定向符号 '<'
        {
            *pos = '\0'; // ls -a -l -n < filename.txt
            pos++;// 指向重定向符号后面的字符。
            while(isspace(*pos)) pos++;// 跳过空格。
            rdirfilename = pos;// 记录重定向的文件名。
            rdir=IN_RDIR;// 设置重定向类型为输入。
            break;// 跳出循环。
        }
        else{
            //do nothing
        }
        pos++;// 指向下一个字符。
    }
}

// 'interact()' 函数实现了shell的交互过程，显示命令行提示符，并读取用户输入的命令。
// 调用 'getpwd()' 获取当前路径，调用 'check_redir()' 检查并处理重定向。
void interact(char *cline, int size)// 定义了交互函数。
{
    getpwd();// 获取当前路径。
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname1(), pwd);// 显示命令行提示符。
    char *s = fgets(cline, size, stdin);// 从标准输入读取一行命令。
    assert(s);// 断言，确保读取成功。
    (void)s;// 避免编译器警告。
    // "abcd\n\0"
    cline[strlen(cline)-1] = '\0';// 将 '\n' 字符替换为 '\0'。

    //ls -a -l > myfile.txt
    check_redir(cline);// 检查并处理重定向。
}

// 'splitstring()' 函数将用户输入的命令行字符串 cline 按照 DELIM 中定义的分隔符分割成参数数组 _argv ，并返回参数个数。
int splitstring(char cline[], char *_argv[])// 定义了分割字符串函数。
{
    int i = 0;// 数组下标。
    argv[i++] = strtok(cline, DELIM);// 第一个参数。
    while(_argv[i++] = strtok(NULL, DELIM)); // 故意写的=
    return i - 1;// 返回参数个数。
}

// 'NormalExcute()' 函数用于执行外部命令。它通过 'fork()' 创建子进程，在子进程中使用 'execvp()' 执行命令，并根据 rdir 的值进行相应的重定向操作。
// 父进程通过 'waitpid()' 等待子进程结束，并获取子进程的退出状态码。
void NormalExcute(char *_argv[])// 定义了普通命令执行函数。
{
    pid_t id = fork();// 创建子进程。
    if(id < 0){// 创建失败。
        perror("fork");// 输出错误信息。
        return;// 退出函数。
    }
    else if(id == 0){// 子进程。
        int fd = 0;// 文件描述符。

        // 后面我们做了重定向的工作，后面我们在进行程序替换的时候，难道不影响吗? 
        // 这里的重定向操作应该是先关闭文件描述符，然后再打开文件，这样才可以实现重定向的功能。
        // 但是，这里的重定向操作是先打开文件，然后再关闭文件描述符，这样就导致了文件描述符的混乱。
        // 所以，这里的重定向操作应该是先打开文件，然后再关闭文件描述符，这样才可以实现重定向的功能。

        if(rdir == IN_RDIR)// 输入重定向。'OUT_RDIR'定义了输出重定向类型。
        {
            fd = open(rdirfilename, O_RDONLY);// 打开输入文件。'O_RDONLY'是只读模式。
            dup2(fd, 0);// 复制文件描述符。
        }
        else if(rdir == OUT_RDIR)// 输出重定向。
        {
            fd = open(rdirfilename, O_CREAT|O_WRONLY|O_TRUNC, 0666);// 打开输出文件。'O_CREAT'是创建文件，'O_WRONLY'是只写模式，'O_TRUNC'是截断模式。
            dup2(fd, 1);// 复制文件描述符。
        }
        else if(rdir == APPEND_RDIR)// 追加重定向。'APPEND_RDIR'定义了追加重定向类型。
        {
            fd = open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND,0666);// 打开追加文件。'O_CREAT'是创建文件，'O_WRONLY'是只写模式，'O_APPEND'是追加模式。
            dup2(fd, 1);// 复制文件描述符。
        }
        //让子进程执行命令
        //execvpe(_argv[0], _argv, environ);
        execvp(_argv[0], _argv);//目的就是执行外部命令，并替换当前进程。
        exit(EXIT_CODE);// 子进程退出。'EXIT_CODE'定义了命令执行失败时的退出码。
    }
    else{// 父进程。
        int status = 0;// 子进程退出状态码。
        pid_t rid = waitpid(id, &status, 0);// 等待子进程结束。
        if(rid == id) // 子进程结束。
        {
            lastcode = WEXITSTATUS(status);// 获取子进程的退出状态码。'WEXITSTATUS'宏用于获取退出状态码。
        }
    }
}

// 'buildCommand()' 函数用于处理特殊命令，如 cd, export, echo。
// 它通过 strcmp() 函数判断命令是否为 cd, export, echo，并进行相应的处理。
// 如 cd 命令，它通过 chdir() 函数切换当前工作目录，并调用 getpwd() 函数获取当前路径，并将其存储在环境变量 PWD 中。
// 如 export 命令，它通过 strcpy() 函数拷贝环境变量字符串，并调用 putenv() 函数将其添加到环境变量表中。
// 如 echo 命令，它通过 strcmp() 函数判断参数是否为 $?，如果是，则输出上一个命令的退出状态码；如果参数以 $ 开头，则输出环境变量的值；否则，输出参数字符串。
int buildCommand(char *_argv[], int _argc)
{
    if(_argc == 2 && strcmp(_argv[0], "cd") == 0){// 切换目录命令。
        chdir(argv[1]);// 切换目录。
        getpwd();// 获取当前路径。
        sprintf(getenv("PWD"), "%s", pwd);// 将当前路径存储在环境变量 PWD 中。
        return 1;// 返回1表示命令执行成功。
    }
    else if(_argc == 2 && strcmp(_argv[0], "export") == 0){// 环境变量设置命令。
        strcpy(myenv, _argv[1]);// 拷贝环境变量字符串。
        putenv(myenv);// 将环境变量添加到环境变量表中。
        return 1;// 返回1表示命令执行成功。
    }
    else if(_argc == 2 && strcmp(_argv[0], "echo") == 0){// 输出命令。
        if(strcmp(_argv[1], "$?") == 0)// 输出上一个命令的退出状态码。
        {
            printf("%d\n", lastcode);// 输出上一个命令的退出状态码。
            lastcode=0;// 重置上一个命令的退出状态码。
        }
        else if(*_argv[1] == '$'){// 输出环境变量的值。
            char *val = getenv(_argv[1]+1);// 获取环境变量的值。
            if(val) printf("%s\n", val);// 输出环境变量的值。
        }
        else{// 输出参数字符串。
            printf("%s\n", _argv[1]);// 输出参数字符串。
        }

        return 1;// 返回1表示命令执行成功。
    }

    // 特殊处理一下ls
    if(strcmp(_argv[0], "ls") == 0)
    {
        _argv[_argc++] = "--color";// 添加参数 --color 使 ls 命令输出颜色。 
        _argv[_argc] = NULL;// 最后一个参数置为空。
    }
    return 0;// 返回0表示命令不在特殊命令列表中。
}

int main()//主函数的隐式默认输入参数是int argc, char *argv[]。'argc' 代表命令行参数个数，'argv' 代表命令行参数数组。 
{
    while(!quit){// 循环执行命令。  
        // 1. 重置重定向变量
        rdirfilename = NULL;// 重置重定向文件名。
        rdir = NONE;// 重置重定向类型。

        // 2. 交互问题,获取命令行, ls -a -l > myfile / ls -a -l >> myfile / cat < file.txt
        interact(commandline, sizeof(commandline));// 获取命令行。 
        //'interact()' 函数实现了交互过程，显示命令行提示符，并读取用户输入的命令。 'commandline' 存储用户输入的命令行字符串。

        // commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"
        // 3. 子串分割的问题，解析命令行
        int argc = splitstring(commandline, argv);// 解析命令行。 'argv' 存储解析后的命令参数数组。
        if(argc == 0) continue;// 空命令，跳过。

        // 4. 指令的判断 
        // debug 
        //for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
        //内键命令，本质就是一个shell内部的一个函数 
        int n = buildCommand(argv, argc);// 处理特殊命令。 'buildCommand()' 函数用于处理特殊命令，如 cd, export, echo。

        // 5. 普通命令的执行
        if(!n) NormalExcute(argv);// 普通命令的执行。 'NormalExcute()' 函数用于执行外部命令。
    }
    return 0;// 退出程序。
}